/*

  Launcher for external processes 


  Copyright © 2011-2 F.Hroch (hroch@physics.muni.cz)

  This file is part of Munipack.

  Munipack is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.
  
  Munipack is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  
  You should have received a copy of the GNU General Public License
  along with Munipack.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "mprocess.h"
#include <wx/wx.h>
#include <wx/app.h>
#include <wx/stream.h>
#include <wx/txtstrm.h>
#include <wx/wfstream.h>
#include <wx/file.h>
#include <wx/log.h>
#include <wx/event.h>
#include <wx/process.h>
#include <iostream>
#include <queue>
#include <stdio.h>


using namespace std;

// ----

MuniProcess * operator<< (MuniProcess *p, const wxString& t)
{
  p->Write(t);
  return p;
}

// ----- 

MuniProcess::MuniProcess(wxEvtHandler *h, const wxString& c, const wxArrayString& args): 
  wxProcess(h),command(c),ArgsBuffer(args),exitcode(-1),handler(h),timer(this),killing(false)
{
  argv = static_cast<wchar_t **>(malloc((args.GetCount()+2)*sizeof(wchar_t *)));
  argv[0] = wxStrdup(command.wc_str());
  size_t l = 1;
  for(size_t i = 0; i < args.GetCount(); i++)
    argv[l++] = wxStrdup(args[i].wc_str());
  argv[l] = 0;

  Redirect();

  Bind(wxEVT_TIMER,&MuniProcess::OnTimer,this);
  Bind(wxEVT_END_PROCESS,&MuniProcess::OnFinish,this);
}

MuniProcess::~MuniProcess()
{
  if( argv ) {
    for(size_t i = 0; argv[i] != 0; i++)
      free(argv[i]);
    free(argv);
  }
}

void MuniProcess::OnStart()
{
  wxLogDebug("Launching `" + command + "' ...");

  // Switch-off buffering of gfortran's stdout and stderr.
  // We need this setting for on-the-fly parsing of outputs.
  wxSetEnv("GFORTRAN_UNBUFFERED_PRECONNECTED","Y");

  long pid = wxExecute(argv,wxEXEC_ASYNC,this);

  if( pid <= 0 ) {
    wxLogDebug("Failed to launch the external command `" + command + "'.");
    return;
  }

  timer.Start(100);

  wxOutputStream *i = GetOutputStream();
  wxASSERT(i && i->IsOk());
  wxTextOutputStream out(*i);

  fitskeys(out);

  if( ! InputBuffer.IsEmpty() ) {
    for(size_t i = 0; i < InputBuffer.GetCount(); i++) {
      out << InputBuffer[i] << endl;
      wxLogDebug(InputBuffer[i]);
    }
  }
  CloseOutput();
    
  wxASSERT(wxProcess::Exists(pid));
}

void MuniProcess::SetInput(const wxArrayString& i)
{ 
  for(size_t l = 0; l < i.GetCount(); l++)
    InputBuffer.Add(i[l]);
}

void MuniProcess::Write(const wxString& line)
{
  InputBuffer.Add(line);
}

wxKillError MuniProcess::Kill(wxSignal sig, int flags)
{
  killing = true;
  return wxProcess::Kill(GetPid(),sig,flags);
}

void MuniProcess::OnTimer(wxTimerEvent& event)
{
  //  wxLogDebug("MuniProcess::OnTimer");
  Flush();
}

void MuniProcess::OnIdle(wxIdleEvent& event)
{
  Flush();

  event.RequestMore();
  event.Skip();
}

void MuniProcess::OnFinish(wxProcessEvent& event)
{
  wxLogDebug("MuniProcess::OnFinish: %d %d",event.GetPid(),event.GetExitCode());

  exitcode = event.GetExitCode();

  timer.Stop();
  Flush();

  /*

    On finish, we must test both the return code (status)
    and a possible error output. The testing of just status
    isn't adequate because the implementation of wxExecute
    returns -1 also when waitpid gives 0 (no changes). 

    Therefore we are suppose the Unix convection: a process
    which had finish wihout errors giving an empty (error) output.
    That is familiar for command-line users as behavior
    of many system commands. 
    
    The outline is little bit complicated by using of some
    Fortran utilities which prints STOP <NUMBER> to indicate
    their return status.

    By the way, this code looks for 'STOP 0' string as the last
    error output of a subprocces. We are ensure that
    the process correctly finished.

    This is also another exception. When user requested killing 
    of a process, we always returns non-zero.

  */

  if( exitcode != 0 && killing == false ) {

    long status = -1;
    
    // accept only non-blank lines
    wxArrayString err;
    for(size_t i = 0; i < ErrorBuffer.GetCount(); i++) {
      wxString a(ErrorBuffer.Item(i).Trim());
      if( ! a.IsEmpty() )
	err.Add(ErrorBuffer.Item(i));
    }

    if( err.IsEmpty() ) {

      // empty error output
      status = 0;
    }
    else {
      
      // Fortran status codes
      for(int i = err.GetCount() - 1; i >= 0; i--) {
	wxString a(err.Item(i).Upper());
	const wxString stop("STOP");
	int pos = a.Find(stop);
	if( pos != wxNOT_FOUND ) {
	  wxLogDebug(a);
	  a.Replace(stop,"");
	  long s;
	  if( a.ToLong(&s) ) {
	    status = s;
	    break;
	  }
	}
      }
    }

    exitcode = status;

    if( status != 0 /* || exitcode != 0*/ ) {

      wxLogMessage("Launching of the external utility `"+command+"' failed:");

      for(size_t i = 0; i < ArgsBuffer.GetCount(); i++)
	wxLogMessage("Arg[%d]: "+ArgsBuffer[i],(int)i);

      for(size_t i = 0; i < InputBuffer.GetCount(); i++)
	wxLogMessage("Input: "+InputBuffer[i]);

      for(size_t i = 0; i < OutputBuffer.GetCount(); i++)
	wxLogMessage("Output: "+OutputBuffer[i]);

      for(size_t i = 0; i < ErrorBuffer.GetCount(); i++)
	wxLogError("Error: "+ErrorBuffer[i]);

      wxLogError("Command `"+command+"' failed with exit code: %d, status: %d",
		 (int)event.GetExitCode(),(int)status);
    }
  }

  wxQueueEvent(handler,new wxProcessEvent(event.GetId(),event.GetPid(),exitcode));
}

void MuniProcess::Flush()
{
  if( IsInputAvailable() ) {
    wxInputStream *i = GetInputStream();
    if( i ) {
      wxTextInputStream out(*i);
      while( i->IsOk() && i->CanRead() ) {
	wxString line = out.ReadLine();
	fprintf(stdout,"%s\n",static_cast<const char *>(line.char_str()));
	OutputBuffer.Add(line);
      }
    }
  }
    
  if( IsErrorAvailable() ) {
    wxInputStream *e = GetErrorStream();
    if( e ) {
      wxTextInputStream err(*e);
      while( e->IsOk() && e->CanRead() ) {
	wxString line = err.ReadLine();
	fprintf(stderr,"%s\n",static_cast<const char *>(line.char_str()));
	ErrorBuffer.Add(line);
      }
    }
  }
}

void MuniProcess::fitskeys(wxTextOutputStream& out)
{
  // redefine FITS keywords

  const int kpos = 9; // position of first char following FITS_KEY_
  const int nkeys = 4;
  const char *keys[nkeys] = { "FITS_KEY_FILTER", 
			      "FITS_KEY_TEMPERAT", 
			      "FITS_KEY_DATE-OBS", 
			      "FITS_KEY_EXPTIME" };

  for(int i = 0; i < nkeys; i++) {
    const char *var = getenv(keys[i]);
    if( var )
      out << "Key " + wxString(keys[i] + kpos) + " = '" + var + "'";
  }
}

void MuniProcess::Write(const wxArrayString& params,
			const wxString& flag, const wxString& ext)
{
  bool stdin = false;
  for(size_t i = 1; i < params.GetCount(); i++) {
    if( params.Item(i) == "-" )
      stdin = true;
    else {
      wxString orig = params.Item(i),result;
      parsename(flag,ext,params.Item(i),orig,result);

      if( result.IsEmpty() )
	InputBuffer.Add("'" + orig + "'");
      else
	InputBuffer.Add("'" + orig + "' '" + result + "'");
    }
  }
    
  if( stdin ) {
    char line[32768];
    while( true ) {
      cin.getline(line,32768);
      if( ! cin.good() ) break;

      wxString orig = line,result;
      parsename(flag,ext,line,orig,result);

      if( result.IsEmpty() )
	InputBuffer.Add("'" + orig + "'");
      else
	InputBuffer.Add("'" + orig + "' '" + result + "'");
    }
  }
}

void MuniProcess::parsename(const wxString& flag, const wxString& suffix, 
			    const wxString& line, wxString& orig,
			    wxString& result)
{

  int pos = line.Find(",");
  if( pos == wxNOT_FOUND ) {
    // x.fits => orig = 'x.fits', result = 'x_D.fits'
    orig = line;
    pos = line.Find('/',true);
    if( pos == wxNOT_FOUND ) {
      int p = line.find(".");
      if( p == wxNOT_FOUND )
	result = line + suffix;
      else
	result = line.Mid(0,p) + suffix + line.Mid(p);
    }
    else {
      wxString base = line.Mid(pos+1);
      int p = base.Find(".");
      if( p == wxNOT_FOUND )
	result = line.Mid(0,pos+1) + base.Mid(0,p) + suffix;
      else
	result = line.Mid(0,pos+1) + base.Mid(0,p) + suffix + base.Mid(p);
    }
  }
  else {
    // x.fits,y.fits => orig = 'x.fits', result = 'y.fits'
    orig = line.Mid(0,pos);
    result = line.Mid(pos+1);
  }

  if( ! flag.IsEmpty() ) {

    if( flag.Find(".") != wxNOT_FOUND ) {
      pos = orig.Find('/',true);
      if( pos == wxNOT_FOUND )
	// x.fits -> x.fits
	result = orig;
      else
	// /home/x.fits -> x.fits
	result = orig.Mid(pos+1);
    }

    if( flag.Find("!") != wxNOT_FOUND )
      // /home/x.fits -> !/home/x.fits
      result = "!" + result;
  }
}



// ------


MuniPipe::MuniPipe(wxEvtHandler *h): handler(h),exitcode(-1)
{
  Bind(wxEVT_END_PROCESS,&MuniPipe::OnFinish,this);
}

MuniPipe::~MuniPipe()
{
  wxASSERT(procs.empty());
}

void MuniPipe::push(MuniProcess *p)
{
  wxASSERT(p);
  procs.push(p);
}

void MuniPipe::Start()
{
  wxASSERT(procs.front());

  MuniProcess *p = procs.front();
  p->SetInput(OutputBuffer);
  p->OnPreProcess();
  p->OnStart();
}

void MuniPipe::Stop()
{
  if( ! procs.empty() ) {
    procs.front()->Kill();

    while( ! procs.empty() )
      procs.pop();

    wxASSERT(procs.empty());
  }
}

void MuniPipe::OnFinish(wxProcessEvent& event)
{
  wxLogDebug("MuniPipe::OnFinish %d %d",event.GetPid(),event.GetExitCode());
  exitcode = event.GetExitCode();

  if( ! procs.empty() ) {
    OutputBuffer = procs.front()->GetOutput();
    ErrorBuffer = procs.front()->GetErrors();
  }

  if( event.GetExitCode() != 0 ) {
    while( ! procs.empty() )
      procs.pop();
  }
  else {
    wxASSERT(procs.front());

    procs.front()->OnPostProcess();
    procs.pop();
  }

  wxLogDebug("Remaning processes: %d",(int) procs.size());

  if( ! procs.empty() )
    Start();

  else if( handler )
    wxQueueEvent(handler,event.Clone());

  else
    event.Skip();
}

wxArrayString MuniPipe::GetOutput() const
{
  if( ! procs.empty() )
    return procs.front()->GetOutput();
  else
    return OutputBuffer;
} 

wxArrayString MuniPipe::GetErrors() const
{
  if( ! procs.empty() )
    return procs.front()->GetErrors();
  else
    return ErrorBuffer;
} 

